package qy.jalgotrade.broker.fillstrategy;

import java.util.HashMap;
import java.util.Map;

import com.google.common.math.DoubleMath;

import qy.jalgotrade.bar.Bar;
import qy.jalgotrade.bar.Bars;
import qy.jalgotrade.broker.Broker;
import qy.jalgotrade.broker.LimitOrder;
import qy.jalgotrade.broker.MarketOrder;
import qy.jalgotrade.broker.Order;
import qy.jalgotrade.broker.StopLimitOrder;
import qy.jalgotrade.broker.StopOrder;
import qy.jalgotrade.broker.backtesting.BacktestingBroker;
import qy.jalgotrade.broker.backtesting.BacktestingStopLimitOrder;
import qy.jalgotrade.broker.backtesting.BacktestingStopOrder;
import qy.jalgotrade.broker.slippage.NoSlippage;
import qy.jalgotrade.broker.slippage.SlippageModel;
import qy.jalgotrade.utils.CommonUtils;

/**
 * <pre>
 * Default fill strategy. 默认的 Order 处理 (成交) 策略, 实现逻辑为 (依据 Order 类型):
 * 
 * pyalgotrade.broker.MarketOrder (市价单): 以 open / close 成交;
 * pyalgotrade.broker.LimitOrder (限价单): 若 限价 被 open 击穿, 则按 open 成交; 若 限价 在 bar 范围内, 则按 限价 成交;
 * pyalgotrade.broker.StopOrder ((市价)止损单): 若 止损价 被 open 击穿, 则按 open 成交, 若 止损价 在 bar 范围内, 则按 止损价 成交;
 * pyalgotrade.broker.StopLimitOrder (限价止损单): 若 止损价 被 open 击穿, 或 止损价 在 bar 范围内, 则 限价止损单 激活;
 *     1) 若 限价止损单 在 当前 bar 激活, 则按 止损价 和 限价 最优者成交 (要求 限价 必须 可达);
 *     2) 若 限价止损单 在 上一个 bar 激活, 则按限价 成交.
 * note 注意:
 *     1. Broker 缺省采用 DefaultStrategy;
 *     2. 默认使用 pyalgotrade.broker.slippage.NoSlippag;
 *     3. 若 volumeLimit 为 0.25, 某个 bar 的 volume 为 100, 则所有 Order 在这个 bar 累计成交不得超过 25;
 *     4. 若 bar 的 周期 为 Tick (pyalgotrade.bar.Frequency.TRADE), 则不受上述 3. 限制.
 * </pre>
 * 
 * <pre>
 * This strategy works as follows:
 * 
 * * A :class:`pyalgotrade.broker.MarketOrder` is always filled using the open/close price.
 * * A :class:`pyalgotrade.broker.LimitOrder` will be filled like this:
 *     * If the limit price was penetrated with the open price, then the open price is used.
 *     * If the bar includes the limit price, then the limit price is used.
 *     * Note that when buying the price is penetrated if it gets <= the limit price, and when selling the price
 *       is penetrated if it gets >= the limit price
 * * A :class:`pyalgotrade.broker.StopOrder` will be filled like this:
 *     * If the stop price was penetrated with the open price, then the open price is used.
 *     * If the bar includes the stop price, then the stop price is used.
 *     * Note that when buying the price is penetrated if it gets >= the stop price, and when selling the price
 *       is penetrated if it gets <= the stop price
 * * A :class:`pyalgotrade.broker.StopLimitOrder` will be filled like this:
 *     * If the stop price was penetrated with the open price, or if the bar includes the stop price, then the limit
 *       order becomes active.
 *     * If the limit order is active:
 *         * If the limit order was activated in this same bar and the limit price is penetrated as well, then the
 *           best between the stop price and the limit fill price (as described earlier) is used.
 *         * If the limit order was activated at a previous bar then the limit fill price (as described earlier)
 *           is used.
 *
 * .. note::
 *     * This is the default strategy used by the Broker.
 *     * It uses :class:`pyalgotrade.broker.slippage.NoSlippage` slippage model by default.
 *     * If volumeLimit is 0.25, and a certain bar's volume is 100, then no more than 25 shares can be used by all
 *       orders that get processed at that bar.
 *     * If using trade bars, then all the volume from that bar can be used.
 * </pre>
 * 
 * @author c-geo
 *
 */
public class DefaultStrategy extends FillStrategy {

	private double __volumeLimit;

	private SlippageModel __slippageModel;

	private Map<String, Double> __volumeLeft;

	private Map<String, Double> __volumeUsed;

	/**
	 * 
	 */
	public DefaultStrategy() {

		this(0.25);
	}

	/**
	 * 
	 * @param volumeLimit
	 *            The proportion of the volume that orders can take up in a bar. Must be > 0 and <=
	 *            1. If None, then volume limit is not checked.
	 */
	public DefaultStrategy(double volumeLimit) {

		super();

		__volumeLeft = new HashMap<>();
		__volumeUsed = new HashMap<>();
		setVolumeLimit(volumeLimit);
		setSlippageModel(new NoSlippage());
	}

	/**
	 * 
	 * @return
	 */
	public Map<String, Double> getVolumeLeft() {

		return __volumeLeft;
	}

	/**
	 * 
	 * @return
	 */
	public Map<String, Double> getVolumeUsed() {

		return __volumeUsed;
	}

	/**
	 * Set the volume limit.
	 * 
	 * @param volumeLimit
	 *            The proportion of the volume that orders can take up in a bar. Must be > 0 and <=
	 *            1. If None, then volume limit is not checked.
	 */
	public void setVolumeLimit(double volumeLimit) {

		assert volumeLimit > 0 && volumeLimit <= 1 : "Invalid volume limit";
		__volumeLimit = volumeLimit;
	}

	/**
	 * Set NO volume limit (Double.NaN).
	 */
	public void setNoVolumeLimit() {

		__volumeLimit = Double.NaN;
	}

	/**
	 * Set the slippage model to use.
	 * 
	 * @param slippageModel
	 *            The slippage model.
	 */
	public void setSlippageModel(SlippageModel slippageModel) {

		__slippageModel = slippageModel;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see qy.jalgotrade.broker.fillstrategy.FillStrategy#onBars(qy.jalgotrade.broker.Broker,
	 * qy.jalgotrade.bar.Bars)
	 */
	@Override
	public void onBars(Broker broker, Bars bars) {

		Map<String, Double> volumeLeft = new HashMap<>();

		for (String instrument : bars.getInstruments()) {
			Bar bar = bars.__getitem__(instrument);
			// Reset the volume available for each instrument.
			if (bar.getFrequency() == Bar.Frequency.TRADE) {
				volumeLeft.put(instrument, bar.getVolume());
			} else if (!Double.isNaN(__volumeLimit)) {
				// We can't round here because there is no order to request the instrument traits.
				volumeLeft.put(instrument, bar.getVolume() * __volumeLimit);
			}
			// Reset the volume used for each instrument.
			__volumeUsed.put(instrument, 0.0);
		}

		__volumeLeft = volumeLeft;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * qy.jalgotrade.broker.fillstrategy.FillStrategy#onOrderFilled(qy.jalgotrade.broker.Broker,
	 * qy.jalgotrade.broker.Order)
	 */
	@Override
	public void onOrderFilled(Broker broker, Order order) {

		// Update the volume left.
		if (!Double.isNaN(__volumeLimit)) {
			// We round the volume left here becuase it was not rounded when it was initialized.
			double volumeLeft = order.getInstrumentTraits().roundQuantity(__volumeLeft.get(order.getInstrument()));
			double fillQuantity = order.getExecutionInfo().getQuantity();
			assert DoubleMath.fuzzyCompare(volumeLeft, fillQuantity,
					CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH) >= 0 : String
							.format("Invalid fill quantity %s. Not enough volume left %s", fillQuantity, volumeLeft);

			__volumeLeft.put(order.getInstrument(),
					(double) order.getInstrumentTraits().roundQuantity(volumeLeft - fillQuantity));
		}

		// Update the volume used.
		__volumeUsed.put(order.getInstrument(), (double) order.getInstrumentTraits()
				.roundQuantity(__volumeUsed.get(order.getInstrument()) + order.getExecutionInfo().getQuantity()));
	}

	/**
	 * 
	 * @param broker
	 * @param order
	 * @param bar
	 * @return
	 */
	private double __calculateFillSize(Broker broker, Order order, Bar bar) {

		double ret = 0;

		double maxVolume;
		// If self.__volumeLimit is None then allow all the order to get filled.
		if (!Double.isNaN(__volumeLimit)) {
			maxVolume = __volumeLeft.getOrDefault(order.getInstrument(), 0.0);
			maxVolume = order.getInstrumentTraits().roundQuantity(maxVolume);
		} else {
			maxVolume = order.getRemaining();
		}

		if (!order.getAllOrNone()) {
			ret = Math.min(maxVolume, order.getRemaining());
		} else if (DoubleMath.fuzzyCompare(order.getRemaining(), maxVolume,
				CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH) <= 0) {
			ret = order.getRemaining();
		}

		return ret;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * qy.jalgotrade.broker.fillstrategy.FillStrategy#fillMarketOrder(qy.jalgotrade.broker.Broker,
	 * qy.jalgotrade.broker.Order, qy.jalgotrade.bar.Bar)
	 */
	@Override
	public FillInfo fillMarketOrder(Broker broker, Order order, Bar bar) {

		// Calculate the fill size for the order.
		double fillSize = __calculateFillSize(broker, order, bar);
		if (DoubleMath.fuzzyEquals(fillSize, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)) {

			((BacktestingBroker) broker).getLogger().debug(
					"Not enough volume to fill {} market order [{}] for {} share/s", order.getInstrument(),
					order.getId(), order.getRemaining());
			return null;
		}

		// Unless its a fill-on-close order, use the open price.
		double price = Double.NaN;
		if (((MarketOrder) order).getFillOnClose()) {
			price = bar.getClose(((BacktestingBroker) broker).getUseAdjustedValues());
		} else {
			price = bar.getOpen(((BacktestingBroker) broker).getUseAdjustedValues());
		}
		assert !Double.isNaN(price);

		// Don't slip prices when the bar represents the trading activity of a single trade.
		if (bar.getFrequency() == Bar.Frequency.TRADE) {
			price = __slippageModel.calculatePrice(order, price, fillSize, bar,
					(double) __volumeUsed.get(order.getInstrument()));
		}

		return new FillInfo(price, fillSize);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * qy.jalgotrade.broker.fillstrategy.FillStrategy#fillLimitOrder(qy.jalgotrade.broker.Broker,
	 * qy.jalgotrade.broker.Order, qy.jalgotrade.bar.Bar)
	 */
	@Override
	public FillInfo fillLimitOrder(Broker broker, Order order, Bar bar) {

		// Calculate the fill size for the order.
		double fillSize = __calculateFillSize(broker, order, bar);
		if (DoubleMath.fuzzyEquals(fillSize, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)) {

			((BacktestingBroker) broker).getLogger().debug(
					"Not enough volume to fill {} limit order [{}] for {} share/s", order.getInstrument(),
					order.getId(), order.getRemaining());
			return null;
		}

		FillInfo ret = null;
		double price = FillStrategy.getLimitPriceTrigger(order.getAction(), ((LimitOrder) order).getLimitPrice(),
				((BacktestingBroker) broker).getUseAdjustedValues(), bar);
		if (!Double.isNaN(price)) {
			ret = new FillInfo(price, fillSize);
		}

		return ret;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * qy.jalgotrade.broker.fillstrategy.FillStrategy#fillStopOrder(qy.jalgotrade.broker.Broker,
	 * qy.jalgotrade.broker.Order, qy.jalgotrade.bar.Bar)
	 */
	@Override
	public FillInfo fillStopOrder(Broker broker, Order order, Bar bar) {

		double fillSize = 0;
		FillInfo ret = null;

		// First check if the stop price was hit so the market order becomes active.
		double stopPriceTrigger = Double.NaN;
		if (!((BacktestingStopOrder) order).getStopHit()) {
			stopPriceTrigger = FillStrategy.getStopPriceTrigger(order.getAction(), ((StopOrder) order).getStopPrice(),
					((BacktestingBroker) broker).getUseAdjustedValues(), bar);
			((BacktestingStopOrder) order).setStopHit(!Double.isNaN(stopPriceTrigger));
		}

		// If the stop price was hit, check if we can fill the market order.
		if (((BacktestingStopOrder) order).getStopHit()) {
			// Calculate the fill size for the order.
			fillSize = __calculateFillSize(broker, order, bar);
			if (DoubleMath.fuzzyEquals(fillSize, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)) {

				((BacktestingBroker) broker).getLogger().debug(
						"Not enough volume to fill {} stop order [{}] for {} share/s", order.getInstrument(),
						order.getId(), order.getRemaining());
				return null;
			}

			/*
			 * If we just hit the stop price we'll use it as the fill price. For the remaining bars
			 * we'll use the open price.
			 */
			double price;
			if (!Double.isNaN(stopPriceTrigger)) {
				price = stopPriceTrigger;
			} else {
				price = bar.getOpen(((BacktestingBroker) broker).getUseAdjustedValues());
			}
			assert !Double.isNaN(price);

			// Don't slip prices when the bar represents the trading activity of a single trade.
			if (bar.getFrequency() != Bar.Frequency.TRADE) {
				price = __slippageModel.calculatePrice(order, price, fillSize, bar,
						__volumeUsed.get(order.getInstrument()));
			}
			ret = new FillInfo(price, fillSize);
		}

		return ret;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see qy.jalgotrade.broker.fillstrategy.FillStrategy#fillStopLimitOrder(qy.jalgotrade.broker.
	 * Broker, qy.jalgotrade.broker.Order, qy.jalgotrade.bar.Bar)
	 */
	@Override
	public FillInfo fillStopLimitOrder(Broker broker, Order order, Bar bar) {

		double fillSize = 0;
		FillInfo ret = null;

		// First check if the stop price was hit so the limit order becomes active.
		double stopPriceTrigger = Double.NaN;
		if (!((BacktestingStopLimitOrder) order).getStopHit()) {
			stopPriceTrigger = FillStrategy.getStopPriceTrigger(order.getAction(),
					((StopLimitOrder) order).getStopPrice(), ((BacktestingBroker) broker).getUseAdjustedValues(), bar);
			((BacktestingStopLimitOrder) order).setStopHit(!Double.isNaN(stopPriceTrigger));
		}

		// If the stop price was hit, check if we can fill the limit order.
		if (((BacktestingStopLimitOrder) order).getStopHit()) {
			// Calculate the fill size for the order.
			fillSize = __calculateFillSize(broker, order, bar);
			if (DoubleMath.fuzzyEquals(fillSize, 0, CommonUtils.FUZZY_EQUAL_TOLERANCE_MATH)) {

				((BacktestingBroker) broker).getLogger().debug(
						"Not enough volume to fill {} stop limit order [{}] for {} share/s", order.getInstrument(),
						order.getId(), order.getRemaining());
				return null;
			}

			double price = FillStrategy.getLimitPriceTrigger(order.getAction(),
					((StopLimitOrder) order).getLimitPrice(), ((BacktestingBroker) broker).getUseAdjustedValues(), bar);
			if (!Double.isNaN(price)) {
				// If we just hit the stop price, we need to make additional checks.
				if (!Double.isNaN(stopPriceTrigger)) {
					if (order.isBuy()) {
						/*
						 * If the stop price triggered is lower than the limit price, then use that
						 * one. Else use the limit price.
						 */
						price = Math.min(stopPriceTrigger, ((StopLimitOrder) order).getLimitPrice());
					} else {
						/*
						 * If the stop price triggered is greater than the limit price, then use
						 * that one. Else use the limit price.
						 */
						price = Math.max(stopPriceTrigger, ((StopLimitOrder) order).getLimitPrice());
					}
				}
				ret = new FillInfo(price, fillSize);
			}
		}

		return ret;
	}
}
